package org.flashamaphone.tones
{
	import flash.utils.ByteArray;
	
	import org.flashamaphone.buffer.SoundConstants;
	import org.flashamaphone.debug.Debugger;
	import org.flashamaphone.tones.ITone;
	import org.flashamaphone.voices.IVoice;
	import org.flashamaphone.voices.Voice;
	
	/** 
	 * A voiced tone implements the standard note lifecycle - strike, sustain, and release
	 */
	public class Tone implements ITone
	{
		public static const PRESS_PHASE:String = "pressPhase";
		public static const HOLD_PHASE:String = "holdPhase";
		public static const UNPRESS_PHASE:String = "unpressPhase";
		public static const OFF_PHASE:String = "offPhase";
		
		/** debug mode */
		protected var _debug:Boolean = false;
		
		/** current phase */
		protected var _phase:String = PRESS_PHASE;
		
		/** frequency of tone */
		protected var _frequency:Number = 440;
		
		/** duration of tone */
		protected var _duration:Number = 1;
		
		/** bytes streamed so far */
		protected var _bytesStreamed:int = 0;
		
		/** unpress bytes streamed so far */
		protected var _unpressBytesStreamed:int = 0;
		
		/** press attack */
		protected var _precachedPress:ByteArray;
		
		/** precached hold */
		protected var _precachedHold:ByteArray;
		
		/** precached let go */
		protected var _precachedUnpress:ByteArray;
		
		/** voice */
		protected var _voice:IVoice = new Voice();
		
		/**
		 * constructor
		 */
		public function Tone() {
			super();
		}

		/**
		 * debug getter
		 * 
		 * @return debug
		 */
		public function get debug():Boolean {
			return _debug;
		}
		
		/**
		 * debug setter
		 * 
		 * @param value
		 */
		public function set debug(value:Boolean):void {
			_debug = value;
		}
		
		/**
		 * frequency getter
		 * 
		 * @return frequency
		 */
		public function get frequency():Number {
			return _frequency;
		}
		
		/**
		 * frequency setter
		 * 
		 * @param frequency
		 */
		public function set frequency(value:Number):void {
			_frequency = value;
		}
		
		/**
		 * frequency duration
		 * 
		 * @return duration
		 */
		public function get duration():Number {
			return _duration;
		}
		
		/**
		 * frequency duration
		 * 
		 * @param duration
		 */
		public function set duration(value:Number):void {
			_duration = value;
		}
		
		/**
		 * voice getter
		 * 
		 * @return voice
		 */
		public function get voice():IVoice {
			return _voice;
		}
		
		/**
		 * voice setter
		 * 
		 * @param voice
		 */
		public function set voice(value:IVoice):void {
			_voice = value;
		}
		
		/** 
		 * get tone byte array
		 * 
		 * @return tone byte array
		 */
		public function get bytes():ByteArray {
			var byts:ByteArray = new ByteArray;
			byts.writeBytes(this.press);
			byts.writeBytes(this.hold);
			byts.writeBytes(this.unpress);
			return byts;
		}
		
		/**
		 * get live bytestream x bytes at a time depending on phase
		 * 
		 * @param number of bytes to get
		 * @return bytes
		 */
		public function stream(numBytes:int):ByteArray {
			// precache initial phase, so we can grab exactly the amount of bytes we need
			// over however many passes this takes
			if (!_precachedPress) {
				_precachedPress = new ByteArray();
				_precachedPress.writeBytes( this.press );
				_precachedPress.position = 0;
			}
			
			// precache the sustain if it's a limited duration
			if (!_precachedHold && voice.holdDuration != -1) {
				_precachedHold = new ByteArray();
				_precachedHold.writeBytes( voice.hold(frequency, voice.holdDuration) );
				_precachedHold.position = 0;
			}
			
			// precache the release if it's a limited duration
			if (!_precachedUnpress) {
				_precachedUnpress = new ByteArray();
				_precachedUnpress.writeBytes( this.unpress );
				_precachedUnpress.position = 0;
			}
			var stream:ByteArray = new ByteArray();
			var offset:int = _bytesStreamed;
			var len:int = numBytes;
			
			// burn through precached attack + decay byte array before moving onto sustain
			if ( phase == PRESS_PHASE ) {
				if (offset + len > _precachedPress.length) {
					len = 0; // zero out to have length ignored and just get the rest of the cache
				}
				stream.writeBytes( _precachedPress, offset, len );
				
				if ( _bytesStreamed + stream.length >= _precachedPress.length ) {
					phase = HOLD_PHASE;
				}
			}
			
			// sustain phase goes on indefinitely if the duration is -1
			len = numBytes - stream.length + 1;
			if (phase == HOLD_PHASE  && len > 0) {
				if (voice.holdDuration == -1) {
					// fill the remaining stream with sustain
					var duration:Number = (len/8)/(SoundConstants.SAMPLING_RATE_MILLISECONDS);
					stream.writeBytes( voice.hold(this.frequency, duration, true) );
				} else {
					offset = _bytesStreamed - _precachedPress.length + stream.length;
					if (offset + len > _precachedHold.length) {
						len = 0; // zero out to have length ignored and just get the rest of the cache
					}
					stream.writeBytes( _precachedHold, 0, len);
					if (_bytesStreamed + stream.length >= _precachedPress.length + _precachedHold.length ) {
						phase = UNPRESS_PHASE;
					}
				}
			}
			
			// burn through precached release byte array before moving onto sustain
			len = numBytes - stream.length;
			offset = _unpressBytesStreamed;
			if ( phase == UNPRESS_PHASE && len > 0 ) {
				if (_precachedUnpress.bytesAvailable > 0) {
					if (len > _precachedUnpress.bytesAvailable) {
						len = _precachedUnpress.bytesAvailable;
					}
					stream.writeBytes( _precachedUnpress, 0, len);
				}
				_unpressBytesStreamed += stream.length;
			}

			_bytesStreamed += stream.length;
			
			if (_unpressBytesStreamed >= _precachedUnpress.length) {
				phase = OFF_PHASE;
			}
			
			if(debug) {
				var color:int;
				switch (phase) {
					case PRESS_PHASE: color = 0x0000ff; break;
					case HOLD_PHASE: color = 0xff0000; break;
					case UNPRESS_PHASE: color = 0x000000; break;
				}
				Debugger.debug(stream, color);
			}
			return stream;
		}
		
		/**
		 * reset byte stream of this note
		 */
		public function resetByteStream():void {
			_bytesStreamed = 0;
			_unpressBytesStreamed = 0;
			if (_precachedUnpress) {
				_precachedUnpress.position = 0;
			}
			if (_precachedPress) {
				_precachedPress.position = 0;
			}
			
			if (_precachedHold) {
				_precachedHold.position = 0;
			}
			phase = PRESS_PHASE;		
		}
		
		/** 
		 * get press bytes
		 */
		public function get press():ByteArray {
			return voice.press(this.frequency, voice.pressDuration);
		}
		
		/** 
		 * get hold bytes
		 */
		public function get hold():ByteArray {
			return voice.hold(this.frequency, voice.holdDuration);
		}
		
		/** 
		 * get unpress bytes
		 */
		public function get unpress():ByteArray {
			return voice.unpress(this.frequency, voice.unpressDuration);
		}
		
		/**
		 * phase getter
		 * 
		 * @return phase
		 */
		public function get phase():String {
			return _phase;
		}
		
		/**
		 * phase setter
		 * 
		 * @param phase
		 */
		public function set phase(value:String):void {
			_phase = value;
		}
		
		/**
		 * clone
		 * 
		 * @param tone
		 * @return tone
		 */
		public function clone():ITone {
			var tone:Tone = new Tone();
			tone.phase = this.phase;
			tone.duration = this.duration;
			tone.voice = this.voice;
			tone.frequency = this.frequency;
			return tone;
		}
	}
}